/*
 * *****************************************************************************
 * Copyright (C) 2014-2024 Dennis Sheirer
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 * ****************************************************************************
 */

package io.github.dsheirer.module.decode.dmr.message;

import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
import java.util.HashMap;
import java.util.Map;

/**
 * Common Announcement Channel is a 24-bit sequence that precedes an outbound DMR frame transmitted by a repeater.
 */
public class CACH
{
    //Generated by method: createHamming7_4BitErrorMap().  The value of the uncorrected 7-bit CACH is used as an index
    //to lookup the bit error position where:
    //   0-6: bit error index location where bits are ordered as transmitted: 0, 1 ..., 6
    //    -1: valid/correct message
    //    -2: invalid message with 2 or more bit errors
    public static final int[] BIT_ERROR_INDEXES = new int[]{-2, -2, -2, -2, -2, -2, -2, -2, -2, 5, 6, -1, 1, 2, 0, 4,
            -2, 1, 4, 0, 5, 3, -1, 6, 0, 4, 1, 2, 6, -1, 3, 5, -2, -2, -2, -2, -2, -2, -2, -2, 4, 0, 2, 1, -1, 6, 5, -2,
            6, -1, 3, 5, 0, 4, 1, -2, 5, 3, -1, 6, 2, 1, 4, 0, -2, -2, -2, -2, -2, -2, -2, -2, 2, 1, 4, 0, 5, -2, -1, 6,
            3, 5, 6, -1, 1, -2, 0, 4, -1, 6, 5, 3, 4, 0, 2, 1, -2, -2, -2, -2, -2, -2, -2, -2, 6, -1, -2, 5, 0, 4, 1, 2,
            4, 0, -2, 1, -1, 6, 5, 3, 1, 2, 0, 4, 3, 5, 6, -1};

    private static final IntField VALUE_FIELD = IntField.range(0, 6);

    private static final int CACH_MESSAGE_LENGTH = 24;
    private static final int[] INTERLEAVE_MATRIX = new int[]{0, 4, 8, 12, 14, 18, 22, 1, 2, 3, 5, 6, 7, 9, 10, 11, 13,
        15, 16, 17, 19, 20, 21, 23};
    private static final int INBOUND_CHANNEL_ACCESS_TYPE = 0;
    private static final int OUTBOUND_BURST_TIMESLOT = 1;
    private static final int[] LINK_CONTROL_START_STOP = new int[]{2, 3};
    private static final int[] CACH_CRC = new int[]{4, 5, 6};
    private static final int[] CHECKSUMS = new int[]{5, 7, 6, 3};
    private static final int PAYLOAD_START = 7;
    private static final int PAYLOAD_END = 24;

    public enum AccessType {IDLE, BUSY};

    private CorrectedBinaryMessage mMessage;
    private boolean mValid;

    /**
     * Constructs an instance.  Note: constructor is private.  Use the getCACH() method to extract a CACH from a
     * raw transmitted DMR burst frame.
     *
     * @param message containing the deinterleaved and error corrected CACH binary message
     * @param valid to indicate if the deinterleaved message passed CRC check
     */
    public CACH(CorrectedBinaryMessage message, boolean valid)
    {
        mMessage = message;
        mValid = valid;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        if(!isValid())
        {
            sb.append("[CRC-ERROR] ");
        }
        sb.append(getInboundChannelAccessType().name()).append(" ");
        sb.append(getLCSS());
        sb.append(" TS:").append(getTimeslot());
        sb.append(" CRC:").append(getCrcChecksum(getMessage()));
        sb.append(" PAYLOAD:").append(getPayload().toHexString());
        sb.append(" MSG:").append(getMessage().toHexString()).append(" ");
        return sb.toString();
    }

    /**
     * Descrambled message bits for this cach
     */
    public CorrectedBinaryMessage getMessage()
    {
        return mMessage;
    }

    /**
     * Indicates if this CACH has the MSB flipped to indicate Hytera XPT cach scrambling.
     */
    public boolean hasHyteraCachScrambling()
    {
        if(!isValid())
        {
            BinaryMessage copy = getMessage().copy();
            copy.flip(0);
            if(getCrcChecksum(copy) == 0)
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Indicates if this cach passes the FEC error correction and indicates that the inbound channel Access Type,
     * Timeslot and LCSS information is valid.
     */
    public boolean isValid()
    {
        return mValid;
    }

     /**
     * Indicates the state of the next inbound channel timeslot, IDLE or BUSY
     */
    public AccessType getInboundChannelAccessType()
    {
        if(mMessage.get(INBOUND_CHANNEL_ACCESS_TYPE))
        {
            return AccessType.BUSY;
        }
        else
        {
            return AccessType.IDLE;
        }
    }

    /**
     * Link Control or CSBK Start/Stop fragment indicator
     */
    public LCSS getLCSS()
    {
        return LCSS.fromValue(mMessage.getInt(LINK_CONTROL_START_STOP));
    }

    /**
     * Indicates the outbound timeslot for the next frame that follows this CACH, 1 or 2
     */
    public int getTimeslot()
    {
        return mMessage.get(OUTBOUND_BURST_TIMESLOT) ? 2 : 1;
    }

    /**
     * Indicates if the timeslot is 0
     */
    public boolean isTimeslot1()
    {
        return !mMessage.get(OUTBOUND_BURST_TIMESLOT);
    }

    /**
     * Binary Payload (17-bit) message fragment
     */
    public BinaryMessage getPayload()
    {
        return mMessage.getSubMessage(PAYLOAD_START, PAYLOAD_END);
    }

    /**
     * Utility method to create a CACH from a raw transmitted DMR burst frame.  Performs deinterleaving and error
     * correction.
     *
     * @return constructed CACH message.
     */
    public static CACH getCACH(CorrectedBinaryMessage message)
    {
        CorrectedBinaryMessage decodedMessage = new CorrectedBinaryMessage(CACH_MESSAGE_LENGTH);

        //Deinterleave the transmitted message to create the decoded message
        for(int x = 0; x < CACH_MESSAGE_LENGTH; x++)
        {
            if(message.get(INTERLEAVE_MATRIX[x]))
            {
                decodedMessage.set(x);
            }
        }

        int codeword = decodedMessage.getInt(VALUE_FIELD);
        int bitError = BIT_ERROR_INDEXES[codeword];

        if(bitError >= 0)
        {
            decodedMessage.flip(bitError);
        }

        boolean valid = bitError != -2;
        return new CACH(decodedMessage, valid);
    }

    /**
     * Calculates the CRC checksum residual.  Calculates what the checkcum should be and XORs this value with the
     * transmitted checksum and returns the residual value.  A residual of 0 indicates a valid CRC check.
     * @param message to check
     * @return checsum residual value
     */
    public static int getCrcChecksum(BinaryMessage message)
    {
        //Perform error detection
        int checksum = message.getInt(CACH_CRC);

        for(int x = 0; x < 4; x++)
        {
            if(message.get(x))
            {
                checksum ^= CHECKSUMS[x];
            }
        }

        return checksum;
    }

    public static void createHamming7_4BitErrorMap()
    {
        IntField valueField = IntField.range(0, 6);
        Map<Integer, Integer> map = new HashMap<>();

        //Link Control Start/Stop value 00 is not valid for CACH, so we'll mark these as invalid codewords (-2).
        int invalidLCSSMask = 0x18; //0011000b

        BinaryMessage parity0 = new BinaryMessage(7);
        parity0.load(4, 3, 5);
        BinaryMessage parity1 = new BinaryMessage(7);
        parity1.load(4, 3, 7);
        BinaryMessage parity2 = new BinaryMessage(7);
        parity2.load(4, 3, 6);
        BinaryMessage parity3 = new BinaryMessage(7);
        parity3.load(4, 3, 3);

        //There are 2^4 = 16 potential valid codewords
        for(int x = 0; x < 16; x++)
        {
            BinaryMessage bm = new BinaryMessage(7);
            bm.load(0, 4, x);

            if(bm.get(0))
            {
                bm.xor(parity0);
            }
            if(bm.get(1))
            {
                bm.xor(parity1);
            }
            if(bm.get(2))
            {
                bm.xor(parity2);
            }
            if(bm.get(3))
            {
                bm.xor(parity3);
            }

            int value = bm.getInt(valueField);

            //Ignore any legal codeword that has a value of 00 in the LCSS field.
            if(((value & invalidLCSSMask) ^ invalidLCSSMask) == invalidLCSSMask)
            {
                continue;
            }

            //Use -1 to indicate no errors and we'll reserve -2 for invalid/non-correctable 2-bit errors.
            map.put(value, -1);

            //Iteratively induce single bit errors and record them into the map
            for(int bitError = 0; bitError < 7; bitError++)
            {
                bm.flip(bitError);
                value = bm.getInt(valueField);

                //Ignore any bit combination with a 00 in the LCSS field.
                if(((value & invalidLCSSMask) ^ invalidLCSSMask) != invalidLCSSMask)
                {
                    map.put(value, bitError);
                }

                bm.flip(bitError);
            }
        }

        StringBuilder sb = new StringBuilder();
        sb.append("public static final int[] BIT_ERROR_INDEXES = new int[]{");

        for(int z = 0; z < 128; z++)
        {
            if(map.containsKey(z))
            {
                sb.append(map.get(z));
            }
            else
            {
                sb.append("-2");
            }

            if(z != 127)
            {
                sb.append(",");
            }
        }

        sb.append("};");

        System.out.println(sb);
    }

    public static void main(String[] args)
    {
        createHamming7_4BitErrorMap();
    }
}